﻿using JESAI.Schedule.Cron.Exceptions;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace JESAI.Schedule.Cron.CronExpressions
{
    public class CronExpressionField : ICronExpressionField
    {
        /// <summary>
        /// The bit array representing the values of the field.
        /// </summary>
        readonly CronExpressionFieldPool pool;

        readonly BitArray bits;
        /* readonly */
        int minValueSet;
        /* readonly */
        int maxValueSet;

        CronExpressionField(CronExpressionFieldPool pool)
        {
            this.pool = pool ?? throw new ArgumentNullException(nameof(pool));
            this.bits = new BitArray(pool.ValueCount);
            this.minValueSet = int.MaxValue;
            this.maxValueSet = -1;
        }

        public static CronExpressionField Parse(CronFieldKind kind, string expression) => TryParse(kind, expression, v => v, static e => throw e());

        public static CronExpressionField? TryParse(CronFieldKind kind, string expression) => TryParse(kind, expression, static v => (CronExpressionField?)v, _ => null);

        public static T TryParse<T>(CronFieldKind kind, string expression, Func<CronExpressionField, T> valueSelector, Func<ExceptionHandle, T> errorSelector)
        {
            if (valueSelector == null) throw new ArgumentNullException(nameof(valueSelector));
            if (errorSelector == null) throw new ArgumentNullException(nameof(errorSelector));

            var field = new CronExpressionField(CronExpressionFieldPool.FromKind(kind));
            var error = field.pool.TryParse(expression, field.Accumulate, null, static e => e);
            return error == null ? valueSelector(field) : errorSelector(error);
        }


        /// <summary>
        /// Parses a crontab field expression representing seconds.
        /// </summary>

        public static CronExpressionField Seconds(string expression) => Parse(CronFieldKind.Second, expression);

        /// <summary>
        /// Parses a crontab field expression representing minutes.
        /// </summary>

        public static CronExpressionField Minutes(string expression) => Parse(CronFieldKind.Minute, expression);

        /// <summary>
        /// Parses a crontab field expression representing hours.
        /// </summary>

        public static CronExpressionField Hours(string expression) => Parse(CronFieldKind.Hour, expression);

        /// <summary>
        /// Parses a crontab field expression representing days in any given month.
        /// </summary>

        public static CronExpressionField Days(string expression) => Parse(CronFieldKind.Day, expression);

        /// <summary>
        /// Parses a crontab field expression representing months.
        /// </summary>

        public static CronExpressionField Months(string expression) => Parse(CronFieldKind.Month, expression);

        /// <summary>
        /// Parses a crontab field expression representing days of a week.
        /// </summary>

        public static CronExpressionField DaysOfWeek(string expression) => Parse(CronFieldKind.DayOfWeek, expression);

        /// <summary>
        /// Parses a crontab field expression representing year.
        /// </summary>

        public static CronExpressionField Year(string expression) => Parse(CronFieldKind.Year, expression);

        /// <summary>
        /// Parses a crontab field expression representing days of a workday.
        /// </summary>

        public static CronExpressionField Workday(string expression) => Parse(CronFieldKind.Workday, expression);

        /// <summary>
        /// Gets the first value of the field or -1.
        /// </summary>

        public int GetFirst() => this.minValueSet < int.MaxValue ? this.minValueSet : -1;

        /// <summary>
        /// Gets the next value of the field that occurs after the given start value or -1 if there is
        /// no next value available.
        /// </summary>

        public int Next(int start)
        {
            if (start < this.minValueSet)
                return this.minValueSet;

            var startIndex = ValueToIndex(start);
            var lastIndex = ValueToIndex(this.maxValueSet);

            for (var i = startIndex; i <= lastIndex; i++)
            {
                if (this.bits[i])
                    return IndexToValue(i);
            }

            return -1;
        }

        int IndexToValue(int index) => index + this.pool.MinValue;
        int ValueToIndex(int value) => value - this.pool.MinValue;

        /// <summary>
        /// Determines if the given value occurs in the field.
        /// </summary>

        public bool Contains(int value) => this.bits[ValueToIndex(value)];

        /// <summary>
        /// Accumulates the given range (start to end) and interval of values into the current set of
        /// the field.
        /// </summary>
        /// <remarks>
        /// To set the entire range of values representable by the field, set <param name="start" /> and
        /// <param name="end" /> to -1 and <param name="interval" /> to 1.
        /// </remarks>

        T Accumulate<T>(int start, int end, int interval, T success, Func<ExceptionHandle, T> errorSelector)
        {
            var minValue = this.pool.MinValue;
            var maxValue = this.pool.MaxValue;

            if (start == end)
            {
                if (start < 0)
                {
                    //
                    // We're setting the entire range of values.
                    //

                    if (interval <= 1)
                    {
                        this.minValueSet = minValue;
                        this.maxValueSet = maxValue;
                        this.bits.SetAll(true);
                        return success;
                    }

                    start = minValue;
                    end = maxValue;
                }
                else
                {
                    //
                    // We're only setting a single value - check that it is in range.
                    //

                    if (start < minValue)
                        return OnValueBelowMinError(start);

                    if (start > maxValue)
                        return OnValueAboveMaxError(start);
                }
            }
            else
            {
                //
                // For ranges, if the start is bigger than the end value then
                // swap them over.
                //

                if (start > end)
                {
                    end ^= start;
                    start ^= end;
                    end ^= start;
                }

                if (start < 0)
                    start = minValue;
                else if (start < minValue)
                    return OnValueBelowMinError(start);

                if (end < 0)
                    end = maxValue;
                else if (end > maxValue)
                    return OnValueAboveMaxError(end);
            }

            if (interval < 1)
                interval = 1;

            int i;

            //
            // Populate the _bits table by setting all the bits corresponding to the valid field values.
            //

            for (i = start - minValue; i <= (end - minValue); i += interval)
                this.bits[i] = true;

            //
            // Make sure we remember the minimum value set so far Keep track of the highest and lowest
            // values that have been added to this field so far.
            //

            if (this.minValueSet > start)
                this.minValueSet = start;

            i += minValue - interval;

            if (this.maxValueSet < i)
                this.maxValueSet = i;

            return success;

            T OnValueAboveMaxError(int value) =>
                errorSelector(
                    () => new CronException(
                        $"{value} is higher than the maximum allowable value for the [{this.pool.Kind}] field. " +
                        $"Value must be between {this.pool.MinValue} and {this.pool.MaxValue} (all inclusive)."));

            T OnValueBelowMinError(int value) =>
                errorSelector(
                    () => new CronException(
                        $"{value} is lower than the minimum allowable value for the [{this.pool.Kind}] field. " +
                        $"Value must be between {this.pool.MinValue} and {this.pool.MaxValue} (all inclusive)."));
        }

        public override string ToString() => ToString(null);

        public string ToString(string? format)
        {
            using var writer = new StringWriter(CultureInfo.InvariantCulture);

            switch (format)
            {
                case "G":
                case null:
                    Format(writer, true);
                    break;
                case "N":
                    Format(writer);
                    break;
                default:
                    throw new FormatException();
            }

            return writer.ToString();
        }

        public void Format(TextWriter writer) => Format(writer, false);

        public void Format(TextWriter writer, bool noNames) => this.pool.Format(this, writer, noNames);

    }
}
